2

原生 JS 实现对 html 元素的拖拽

一、背景介绍

此处为铺垫内容,可跳过】 随着 Web 前端的不断发展,各种各样的前端规范和新知识、新技术层出不穷,极大地拓展了开发者的操作空间,也大大地提升了用户体验。而随着移动端的不断发展,在移动端,人机交互方式发生了很大转变,新的人机交互方式对提供给用户的 “动作” 有了更高要求,例如拖拽功能,就是在移动端会经常接触到的功能。此文就详细地讲解一下,一个简单的拖拽功能的实现(拖拽元素可改变,浏览器窗口边界检测)。

二、知识准备

为了让各位对此文内容有更深刻的掌握,需要掌握如下知识。

1、clientXclientY 属性

clientXclientY ) 事件属性返回当事件被触发时鼠标指针相对于浏览器当前可视窗口的水平(垂直)坐标。
注意:不包括工具栏和滚动条

2、offsetTopoffsetLeft 属性

offsetTop ( offsetLeft ),指的是子元素距离其父元素的上边框(左边框)的偏移量,在不同的浏览器中其值不同,且与父元素的 position 属性( position: static; 除外)有关。在不同浏览以及不同 position 下的具体值,读者可以自行查阅相关资料,因其内容较多,就不展开论述了。

3、clientWidthclientHeight属性

Element.clientWidthElement.clientHeight )属性表示元素的内部宽度,以像素计。该属性包括内边距,但不包括垂直滚动条(如果有)、边框和外边距。该属性值会被四舍五入为一个整数。如果你需要一个小数值,可使用 element.getBoundingClientRect()

4、setCapturereleaseCapture 方法

MDN 对 SetCapture() 函数的说明为:“该函数在属于当前线程的指定窗口里设置鼠标捕获。一旦窗口捕获了 鼠标,所有鼠标输入都针对该窗口,无论光标是否在窗口的边界内。同一时刻只能有一个窗口捕获鼠标。如果鼠标光标在另一个线程创建的窗口上,只有当鼠标键按下时系统才将鼠标输入指向指定的窗口。”

通俗来讲,举个栗子:一只羊被一根有弹性的绳子( SetCapture )拴在木桩,羊可以在绳子可以延展的范围内 随意活动,但永远无法摆脱绳子的束缚。除非有其他因素导致绳子断了(使用了 ReleaseCapture 或点击了其他窗口)。

ReleaseCapture() 用来释放鼠标捕获,当不再需要继续获得鼠标消息就要应该调用 ReleaseCapture() 释放掉,否则别的线程想捕获鼠标事件就会失败。注意:SetCapture()ReleaseCapture() 必须成对出现。

三、实现思路

如果想对元素进行拖拽,那么必须使用三个事件,并且这三个事件的使用顺序不能打乱。

1、onmousedown:鼠标按下事件
2、onmousemove:鼠标移动事件
3、onmouseup:鼠标抬起事件

拖拽的基本原理就是根据鼠标的移动来移动被拖拽的元素。鼠标的移动也就是 x、y 坐标的变化;元素的移动就是元素 position 属性的 topleft 值的改变。当然,并不是任何时候移动鼠标都要造成元素的移动,而应该判断鼠标左键的状态是否为按下状态,是否是在可拖拽的元素上按下的。具体过程如下:

拖拽状态 = false

鼠标在元素上按下之后 {
    拖拽状态 = true
    设置鼠标捕获 
    记录下鼠标的 x,y 坐标
    记录下元素的 x,y 坐标
}

鼠标在元素上移动时 {
    若拖拽状态为 false 就什么也不做
    如果拖拽状态是 true,那么
        元素的 y 坐标 = 现在鼠标 y - 原来鼠标 y + 原来元素 y
        元素的 x 坐标 = 现在鼠标 x - 原来鼠标 x + 原来元素 x
}

鼠标抬起时 {
    拖拽状态 = false
}

四、完整源码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>原生JS实现元素拖拽</title>
    <style>
        *{
            margin: 0;
            height: 0;
        }
        #dragDiv{
            top: 0;
            left: 0;
            width: 100px;
            height: 100px;
            cursor: move;    /*鼠标呈拖拽状*/  
            position: absolute;    /*设置绝对定位,脱离文档流,便于拖拽时计算坐标*/  
            background-color: red;
        }
    </style>
</head>

<body>
    
<div id="dragDiv"></div>
    
<script>

    window.onload = function(){
    
        var oDiv = document.getElementById("dragDiv");    // 获取到要拖拽的元素
        drag(oDiv);    // 调用自己封装的拖拽函数
        
        function drag(obj){
            obj.onmousedown = function(e){
            
                var e = e || window.event;    // 兼容 IE
                // 鼠标点击物体那一刻相对于物体左侧边框的距离=点击时的位置相对于浏览器
                // 最左边的距离-物体左边框相对于浏览器最左边的距离,纵向同理
                var divX = e.clientX - this.offsetLeft;
                var divY = e.clientY - this.offsetTop;
                
                if(obj.setCapture){
                    obj.setCapture();    // 修复低版本 IE bug
                }
                
                document.onmousemove = function(e){
                
                    var e = e || window.event;
                    
                    var disX = e.clientX - divX;
                    var disY = e.clientY - divY;
                    
                    // 控制拖拽物体的范围只能在浏览器视窗内,不允许出现滚动条或拖出可视区域
                    if ( disX < 0 ) {
                        disX = 0;
                    } else if ( disX > document.documentElement.clientWidth - obj.offsetWidth ) {
                        disX = document.documentElement.clientWidth - obj.offsetWidth;
                    }
                    
                    if ( disY < 0 ) {
                        disY = 0;
                    } else if ( disY > document.documentElement.clientHeight - obj.offsetHeight ) {
                        disY = document.documentElement.clientHeight - obj.offsetHeight;
                    }
                    
                    // 移动时重新得到物体的距离,解决拖动时出现晃动现象  
                    obj.style.top = disY + "px";
                    obj.style.left = disX + "px";
                    
                    document.onmouseup = function(){    // 鼠标抬起时不再移动  
                        // 预防鼠标弹起来后还会循环(即预防鼠标放上去的时候还会移动)
                        document.onmousedown = document.onmousemove = null;
                        if( obj.releaseCapture ){
                            obj.releaseCapture();    // 修复低版本 IE bug
                        }
                    }
                }
            }
        }
    }

</script>

</body>
</html>

注意事项
1、onmousedown 事件中的操作对象为拖拽元素,而 onmousemoveonmouseup 事件中的操作对象为 document,这是因为,点击某物体时,用需要拖拽的对象即可,onmousemoveonmouseup 是全局区域,也就是整个文档通用,应该使用 document 对象而不是被拖拽的对象(否则,采用拖拽对象时物体只能往右方或下方移动)

2、之所以使用 setCapture()releaseCapture(),其目的是为了修复低版本 IE 的 bug。在低版本 IE 下,当我们在要拖动的元素上,按下鼠标按钮拖动时,当拖动过快,或者是超出浏览器的文档窗口时,拖动对象身上的 onmousedown 事件就会失效。在 Chrome 我们可以为 doucment 绑定 onmouseout 事件来判断是否发生这样的情况,但是 IE 下却行不通,所以最好的解决办法就时为要拖动的元素对象锁定鼠标事件,在拖动后再解除事件锁定。在本例中,这两个方法用于 onmousedownonmouseup 中。

3、另外,在 Firefox 中有相似的功能,它们分别是:

  • captureEvents ( Event.eventType )
  • releaseEvents ( Event.eventType )

五、案例总结

虽然元素的拖拽算是一个比较基础的知识点,但在实现的过程中,有许多细节需要注意,例如计算坐标的时候,对那几个属性的了解程度,再例如,事件的触发顺序,还有,IE 中的事件获取, setCapture()releaseCapture() 等。

虽然 H5 直接提供了拖拽 API,但为了兼容性,小伙伴们还是需要用 js 去处理的。上例中,虽然对拖拽做了一定的兼容性处理和封装,拖拽对象可以是 div,图片,文字等,但总的来说,是一个比较基础的实现,但有了这个原型,小伙伴们可以根据自己的需求,再加以封装和拓展,例如限定拖拽方向、范围、速度等等。有任何疑问或建议,可以在评论区留言哦,转载请注明出处。


苏格拉没有底
121 声望9 粉丝

当你的才华支撑不起你的野心的时候,那就静下心来,学习!